package org.unidata.mdm.rest.v1.data.service.search;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.AttributedElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.IndexedElement;
import org.unidata.mdm.core.type.security.SecurityLabel;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.type.search.EntityIndexType;
import org.unidata.mdm.meta.type.search.EtalonIndexType;
import org.unidata.mdm.meta.type.search.RecordHeaderField;
import org.unidata.mdm.meta.type.search.RelationHeaderField;
import org.unidata.mdm.rest.system.service.TypedRestInputRenderer;
import org.unidata.mdm.rest.v1.data.module.DataRestModule;
import org.unidata.mdm.rest.v1.data.ro.search.ComplexSearchDataQueryRO;
import org.unidata.mdm.rest.v1.data.ro.search.SearchDataType;
import org.unidata.mdm.rest.v1.data.ro.search.SimpleSearchDataQueryRO;
import org.unidata.mdm.rest.v1.search.converter.SearchFieldsConverter;
import org.unidata.mdm.rest.v1.search.ro.SearchRequestRO;
import org.unidata.mdm.rest.v1.search.ro.SearchSortFieldRO;
import org.unidata.mdm.search.context.ComplexSearchRequestContext.ComplexSearchRequestContextBuilder;
import org.unidata.mdm.search.context.SearchRequestContext;
import org.unidata.mdm.search.context.SearchRequestContext.SearchRequestContextBuilder;
import org.unidata.mdm.search.exception.SearchExceptionIds;
import org.unidata.mdm.search.type.IndexField;
import org.unidata.mdm.search.type.IndexType;
import org.unidata.mdm.search.type.form.FieldsGroup;
import org.unidata.mdm.search.type.form.FormField;
import org.unidata.mdm.search.type.query.FormSearchQuery;
import org.unidata.mdm.search.type.query.SearchQuery;
import org.unidata.mdm.search.type.search.FacetName;
import org.unidata.mdm.search.type.sort.SortField;
import org.unidata.mdm.system.context.InputCollector;
import org.unidata.mdm.system.exception.PlatformFailureException;
import org.unidata.mdm.system.type.configuration.ConfigurationValue;
import org.unidata.mdm.system.type.rendering.FieldDef;
import org.unidata.mdm.system.type.rendering.FragmentDef;
import org.unidata.mdm.system.type.rendering.MapInputSource;
import org.unidata.mdm.system.util.ConvertUtils;

/**
 * Search request renderer
 *
 * @author Alexandr Serov
 * @since 04.12.2020
 **/
public class SearchInputRequestRenderer extends TypedRestInputRenderer<SearchRequestRO> {

    private static final Map<SearchDataType, IndexType> TYPE_MAPPING = Map.of(
        SearchDataType.ETALON, EtalonIndexType.ETALON,
        SearchDataType.RECORD, EntityIndexType.RECORD,
        SearchDataType.RELATION, EntityIndexType.RELATION
    );

    private final MetaModelService metaModelService;
    /**
     * Record system fields.
     */
    private final Map<String, RecordHeaderField> recordHeaderFieldsByName;
    /**
     * Relation system fields.
     */
    private final Map<String, RelationHeaderField> relationHeaderFieldsByName;

    private final ConfigurationValue<Boolean> calculateScore;


    public SearchInputRequestRenderer(MetaModelService metaModelService, ConfigurationValue<Boolean> calculateScore) {
        super(SearchRequestRO.class, Collections.singletonList(FieldDef.fieldDef(DataRestModule.MODULE_ID, ComplexSearchDataQueryRO.class)));
        this.metaModelService = metaModelService;
        this.calculateScore = calculateScore;
        recordHeaderFieldsByName = Arrays.stream(RecordHeaderField.values())
            .collect(Collectors.toMap(IndexField::getPath, Function.identity()));
        relationHeaderFieldsByName = Arrays.stream(RelationHeaderField.values())
            .collect(Collectors.toMap(IndexField::getPath, Function.identity()));
    }

    @Override
    protected void renderFields(FragmentDef fragmentDef, InputCollector collector, SearchRequestRO source, MapInputSource fieldValues) {
        ComplexSearchDataQueryRO searchDataQuery = fieldValues.get(DataRestModule.MODULE_ID, ComplexSearchDataQueryRO.class);
        if (searchDataQuery != null && collector instanceof ComplexSearchRequestContextBuilder) {
            ComplexSearchRequestContextBuilder context = (ComplexSearchRequestContextBuilder) collector;
            SearchDataType searchDataType = searchDataQuery.getSearchDataType();
            List<SimpleSearchDataQueryRO> supplementary = ObjectUtils.defaultIfNull(searchDataQuery.getSupplementary(), Collections.emptyList());
            if (searchDataType != null) {
                context.main(renderSimpleSearchInput(SearchRequestContext.builder(searchDataQuery.getEntity()), searchDataQuery).build());
            }
            for (SimpleSearchDataQueryRO sub : supplementary) {
                SearchDataType subType = sub.getSearchDataType();
                if (subType != null) {
                    String entityName = sub.getEntity();
                    SearchRequestContextBuilder subCtx = SearchRequestContext.builder(entityName);
                    renderSimpleSearchInput(subCtx, sub);
                    context.supplementary(renderSimpleSearchInput(subCtx, sub).build());
                }
            }
        }
    }

    private SearchRequestContextBuilder renderSimpleSearchInput(SearchRequestContextBuilder srcb, SimpleSearchDataQueryRO source) {
        SearchDataType type = source.getSearchDataType();
        IndexType indexType = indexTypeByDataType(type);
        srcb.type(indexType)
            .count(source.getCount())
            .page(source.getPage() > 0 ? source.getPage() - 1 : source.getPage())
            .totalCount(source.isTotalCount())
            .countOnly(source.isCountOnly())
            .fetchAll(source.isFetchAll())
            .source(source.isSource())
            .sayt(source.isSayt())
            .runExits(true)
            .searchAfter(source.getSearchAfter());
        if (EtalonIndexType.ETALON != indexType) {
            EntityElement el = metaModelService.instance(Descriptors.DATA).getElement(source.getEntity());
            IndexedElement iel;
            if (el != null && (iel = el.getIndexed()) != null) {
                Map<String, IndexField> fieldSpace = indexType == EntityIndexType.RECORD ? new HashMap<>(recordHeaderFieldsByName) : new HashMap<>(relationHeaderFieldsByName);
                fieldSpace.putAll(iel.getIndexFields());
                srcb.query(renderMatchSearchQuery(source, fieldSpace)).filter((FormSearchQuery) renderFacetsFilter(source, indexType, el));
                srcb.query(renderFormFields(source, fieldSpace));
                srcb.query(renderFormGroups(source, fieldSpace));
                srcb.returnFields(renderReturnFields(source, indexType));
                srcb.sorting(renderSortFields(source, indexType, fieldSpace));
                srcb.enableScore(!source.isFetchAll() && calculateScore.getValue());
                srcb.joinBy(indexType == EntityIndexType.RECORD ? RecordHeaderField.FIELD_ETALON_ID : RelationHeaderField.FIELD_FROM_ETALON_ID);
            }
        }
        return srcb;
    }

    private SearchQuery renderMatchSearchQuery(SimpleSearchDataQueryRO source, Map<String, IndexField> fieldSpace) {
        Objects.requireNonNull(source, "Source can't be null");
        Objects.requireNonNull(fieldSpace, "FieldSpace can't be null");
        String text = source.getText();
        SearchQuery result = null;
        if (StringUtils.isNotBlank(text)) {
            List<String> searchFields = ObjectUtils.defaultIfNull(source.getSearchFields(), Collections.emptyList());
            List<IndexField> fields = searchFields.stream().map(fieldSpace::get)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
            if (!fields.isEmpty()) {
                result = SearchQuery.matchQuery(source.getText(), false, fields);
            }
        }
        return result;
    }

    private SearchQuery renderFacetsFilter(SimpleSearchDataQueryRO source, IndexType target, AttributedElement el) {
        Objects.requireNonNull(source, "Source can't be null");
        Map<FacetName, Boolean> facets = mapSourceFacets(source.getFacets());
        FieldsGroup and = FieldsGroup.and();

        // 1. Active / inactive records filtering
        if (facets.containsKey(FacetName.FACET_NAME_INACTIVE_ONLY)) {
            and.add(FormField.exact(target == EntityIndexType.RECORD
                ? RecordHeaderField.FIELD_DELETED
                : RelationHeaderField.FIELD_DELETED, Boolean.TRUE));
            // Add active default
        } else {
            and.add(FormField.exact(target == EntityIndexType.RECORD
                ? RecordHeaderField.FIELD_DELETED
                : RelationHeaderField.FIELD_DELETED, Boolean.FALSE));
        }

        // 2. Active / inactive periods filtering
        boolean includeActivePeriods = facets.containsKey(FacetName.FACET_NAME_ACTIVE_PERIODS);
        boolean includeInactivePeriods = facets.containsKey(FacetName.FACET_NAME_INACTIVE_PERIODS);
        // Don't filter for both facets set active
        if ((includeActivePeriods && !includeInactivePeriods) || (!includeActivePeriods && !includeInactivePeriods)) {
            and.add(FormField.exact(target == EntityIndexType.RECORD
                ? RecordHeaderField.FIELD_INACTIVE
                : RelationHeaderField.FIELD_INACTIVE, Boolean.FALSE));
        } else if (!includeActivePeriods) {
            and.add(FormField.exact(target == EntityIndexType.RECORD
                ? RecordHeaderField.FIELD_INACTIVE
                : RelationHeaderField.FIELD_INACTIVE, Boolean.TRUE));
        }

        // 3. All periods (unranged)
        if (!facets.containsKey(FacetName.FACET_UN_RANGED)) {

            Date point = source.getAsOf() == null ? new Date() : ConvertUtils.localDateTime2Date(source.getAsOf());
            if (target == EntityIndexType.RECORD) {
                and.add(FieldsGroup.and(
                    FormField.range(RecordHeaderField.FIELD_FROM, null, point),
                    FormField.range(RecordHeaderField.FIELD_TO, point, null)));
            } else {
                and.add(FieldsGroup.and(
                    FormField.range(RelationHeaderField.FIELD_FROM, null, point),
                    FormField.range(RelationHeaderField.FIELD_TO, point, null)));
            }
        }

        // 4. Operation types
        if (target == EntityIndexType.RECORD) {

            List<OperationType> ot = new ArrayList<>();
            if (facets.containsKey(FacetName.FACET_NAME_OPERATION_TYPE_CASCADED)) {
                ot.add(OperationType.CASCADED);
            }

            if (facets.containsKey(FacetName.FACET_NAME_OPERATION_TYPE_COPY)) {
                ot.add(OperationType.COPY);
            }

            if (facets.containsKey(FacetName.FACET_NAME_OPERATION_TYPE_DIRECT)) {
                ot.add(OperationType.DIRECT);
            }

            if (CollectionUtils.isNotEmpty(ot)) {
                and.add(FormField.exact(RecordHeaderField.FIELD_OPERATION_TYPE, ot.stream()
                    .map(OperationType::name)
                    .collect(Collectors.toList())));
            }
        }

        // 5. Security filters
        List<SecurityLabel> labels = SecurityUtils.getSecurityLabelsForResource(((EntityElement) el).getName());
        if (CollectionUtils.isNotEmpty(labels)) {

            final Map<String, List<SecurityLabel>> grouped = labels.stream()
                .collect(Collectors.groupingBy(SecurityLabel::getName));

            for (Map.Entry<String, List<SecurityLabel>> group : grouped.entrySet()) {

                FieldsGroup osg = FieldsGroup.or();
                for (final SecurityLabel sl : group.getValue()) {

                    FieldsGroup asg = FieldsGroup.and();
                    sl.getAttributes().forEach(sla -> {

                        // First component before '.' separator is the entity name
                        final String attrPath = StringUtils.substringAfter(sla.getPath(), ".");
                        if (StringUtils.isNotEmpty(sla.getValue())) {

                            AttributeElement ame = el.getAttributes().get(attrPath);
                            if (ame.isIndexed()) {
                                asg.add(FormField.exact(ame.getIndexed(), sla.getValue()));
                            }
                        }
                    });

                    if (!asg.isEmpty()) {
                        osg.add(asg);
                    }
                }

                if (!osg.isEmpty()) {
                    and.add(osg);
                }
            }
        }

        if (!and.isEmpty()) {
            return SearchQuery.formQuery(and);
        }

        return null;
    }


    private SearchQuery renderFormFields(SimpleSearchDataQueryRO source, Map<String, IndexField> fieldSpace) {
        Objects.requireNonNull(source, "Source can't be null");
        Objects.requireNonNull(fieldSpace, "FieldSpace can't be null");
        List<FieldsGroup> groups = Optional.ofNullable(source.getFormFields())
            .map(ff -> SearchFieldsConverter.convertFormFields(ff, fieldSpace))
            .orElseGet(Collections::emptyList);
        SearchQuery result = null;
        if (!groups.isEmpty()) {
            result = SearchQuery.formQuery(groups);
        }
        return result;
    }

    private SearchQuery renderFormGroups(SimpleSearchDataQueryRO source, Map<String, IndexField> fieldSpace) {
        Objects.requireNonNull(source, "Source can't be null");
        Objects.requireNonNull(fieldSpace, "FieldSpace can't be null");
        List<FieldsGroup> groups = Optional.ofNullable(source.getFormGroups()).map(fg -> fg.stream()
            .map(g -> SearchFieldsConverter.convertFormFieldsGroup(g, fieldSpace))
            .collect(Collectors.toList()))
            .orElseGet(Collections::emptyList);
        SearchQuery result = null;
        if (!groups.isEmpty()) {
            result = SearchQuery.formQuery(groups);
        }
        return result;
    }

    private List<String> renderReturnFields(SimpleSearchDataQueryRO source, IndexType target) {
        List<String> returnFields = CollectionUtils.isNotEmpty(source.getReturnFields()) ? new ArrayList<>(source.getReturnFields()) : new ArrayList<>();
        if (target == EntityIndexType.RECORD) {
            returnFields.add(RecordHeaderField.FIELD_DELETED.getName());
            returnFields.add(RecordHeaderField.FIELD_ETALON_ID.getName());
        } else {
            returnFields.add(RelationHeaderField.FIELD_DELETED.getName());
            returnFields.add(RelationHeaderField.FIELD_ETALON_ID.getName());
            returnFields.add(RelationHeaderField.FIELD_FROM_ETALON_ID.getName());
            returnFields.add(RelationHeaderField.FIELD_TO_ETALON_ID.getName());
        }
        return returnFields;
    }

    private Collection<SortField> renderSortFields(SimpleSearchDataQueryRO source, IndexType target, Map<String, IndexField> fieldSpace) {
        Collection<SearchSortFieldRO> fields = source.getSortFields();
        List<SortField> result = new ArrayList<>();
        if (fields != null && !fields.isEmpty()) {
            for (SearchSortFieldRO sortFieldRO : fields) {
                IndexField sortRef = fieldSpace.get(sortFieldRO.getField());
                if (sortRef != null) {
                    result.add(SortField.of(sortRef, SortField.SortOrder.valueOf(sortFieldRO.getOrder())));
                }
            }
        } else if (EntityIndexType.RECORD == target) {
            result.add(SortField.of(RecordHeaderField.FIELD_CREATED_AT, SortField.SortOrder.DESC));
        } else {
            result.add(SortField.of(RelationHeaderField.FIELD_CREATED_AT, SortField.SortOrder.DESC));
        }
        return result;
    }

    private Map<FacetName, Boolean> mapSourceFacets(List<String> source) {
        Map<FacetName, Boolean> facets = FacetName.mapFromValues(source);
        if (facets.containsKey(FacetName.FACET_NAME_ACTIVE_ONLY) && facets.containsKey(FacetName.FACET_NAME_INACTIVE_ONLY)) {
            throw new PlatformFailureException(
                "Those facets can't be applied together:" + facets.toString(), SearchExceptionIds.EX_SEARCH_UNAVAILABLE_FACETS_COMBINATION
            );
        }
        return facets;
    }


    private IndexType indexTypeByDataType(SearchDataType dataType) {
        Objects.requireNonNull(dataType, "DataType can't be null");
        IndexType indexType = TYPE_MAPPING.get(dataType);
        if (indexType == null) {
            throw new PlatformFailureException(
                "Unsupported data type:" + dataType, SearchExceptionIds.EX_SEARCH_MAPPING_TYPE_UNKNOWN
            );
        }
        return indexType;
    }

}
